package pt.compflow.graph;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import pt.ipp.isep.gecad.onto2flow.model.Node;
import pt.ipp.isep.gecad.onto2flow.model.Onto2FlowModel;

import com.hp.hpl.jena.ontology.DatatypeProperty;
import com.hp.hpl.jena.ontology.IntersectionClass;
import com.hp.hpl.jena.ontology.ObjectProperty;
import com.hp.hpl.jena.ontology.OntClass;
import com.hp.hpl.jena.ontology.OntModel;
import com.hp.hpl.jena.ontology.OntModelSpec;
import com.hp.hpl.jena.ontology.OntProperty;
import com.hp.hpl.jena.ontology.Ontology;
import com.hp.hpl.jena.ontology.UnionClass;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;
import com.hp.hpl.jena.vocabulary.OWL;
import com.hp.hpl.jena.vocabulary.OWL2;
import com.hp.hpl.jena.vocabulary.RDFS;
import com.hp.hpl.jena.vocabulary.XSD;

public class OntModelGraphWrapperBuilder {
	
	public static final int OBJECTPROPERTY_GRAPH = 1;
	public static final int DATATYPEPROPERTY_GRAPH = 2;
	public static final int SUBCLASSOF_GRAPH = 3;
	
	private OntModel ontModel;
	private Onto2FlowModel targetModel;
	private int type = OBJECTPROPERTY_GRAPH;
	private String graphUri;
	
	private HashSet<String> ontoUris;
	
	public OntModelGraphWrapperBuilder() {
	}
			
	public OntModelGraphWrapper build() {
		ontoUris = new HashSet<String>();
		Iterator<Ontology> ontoIt = ontModel.listOntologies();
		while(ontoIt.hasNext()) ontoUris.add(ontoIt.next().getURI());
		
		OntModelGraphWrapper graph = null;
		if(targetModel != null) graph = new OntModelGraphWrapper(targetModel, graphUri);
		else graph = new OntModelGraphWrapper(graphUri);
		
		// add all nodes
		List<Node> nodeList = new ArrayList<Node>();
		ExtendedIterator<OntClass> it = ontModel.listClasses();
		while(it.hasNext()) {
			OntClass c = it.next();
			
			if(c.isURIResource())
				nodeList.add(graph.addNode(c));
		}
		
		// add all edges
		if(type != SUBCLASSOF_GRAPH) {
			List<OntClass> l = ontModel.listClasses().toList();
			for(OntClass o : l) {
				if(o.isURIResource()) {
					loadRestrictions(graph, o, o);
				} 
			}
		} else {
			loadSubClassOfRels(graph);
		}
		
		return graph;
	}
	
	private void loadSubClassOfRels(OntModelGraphWrapper graph) {
		StmtIterator stmtIt = ontModel.listStatements(null, RDFS.subClassOf, (RDFNode) null);
		while(stmtIt.hasNext()) {
			Statement stmt = stmtIt.next();
			Resource subject = stmt.getSubject();
			RDFNode object = stmt.getObject();
			
			if(!subject.equals(object) && subject.isURIResource() && object.isURIResource()) {
				graph.addEdge(object.asResource(), ontModel.getProperty("/superClassOf"), subject);
			}
		}
	}
	
	private void loadRestrictions(OntModelGraphWrapper graph, OntClass ontClass, OntClass applyToOntClass) {
		
		// Property-centric Restrictions
		// check all domain specifications, since there may be union class domains
		StmtIterator domainStmtIt = ontModel.listStatements(null, RDFS.domain, (RDFNode) null);
		while (domainStmtIt.hasNext()) {
			Statement domainStmt = domainStmtIt.next();
			OntClass domain = domainStmt.getObject().as(OntClass.class);
						
			Property property = domainStmt.getSubject().as(Property.class);
			Resource range = property.getPropertyResourceValue(RDFS.range);
			
			if(range != null) {
			
				boolean ontClassInDomain = false;
				// check the direct domain class
				if(domain.isURIResource() && domain.getURI().compareTo(ontClass.getURI()) == 0) {
					ontClassInDomain = true;
				} else if(domain.isUnionClass()) {
					// check for an union class
					UnionClass domainUnion = domain.asUnionClass();
					Iterator<? extends OntClass> domainClassIt = domainUnion.listOperands();
					while(!ontClassInDomain && domainClassIt.hasNext()) {
						OntClass domainClass = domainClassIt.next();
						if(domainClass.isURIResource() && domainClass.getURI().compareTo(ontClass.getURI()) == 0)
							ontClassInDomain = true;
					}
				}
				
				// if the ontclass belongs to the domain of the property...				
				if(ontClassInDomain) {
					
					if(type == OBJECTPROPERTY_GRAPH && (range.canAs(OntClass.class) || property.canAs(ObjectProperty.class) || range.getURI().compareToIgnoreCase(OWL.Thing.getURI()) == 0))
						loadEdge(graph, applyToOntClass, range, property, true, false, false, false, -1, -1, -1);
					else if(type == DATATYPEPROPERTY_GRAPH && (property.canAs(DatatypeProperty.class) || range.getNameSpace().compareToIgnoreCase(XSD.getURI()) == 0 || range.getURI().compareToIgnoreCase(RDFS.Literal.getURI()) == 0)) {
						graph.addRestrictionEdge(applyToOntClass, property, range, true, false, false, false, -1, -1, -1, isClosure(applyToOntClass, property));
					}
				}
				
			}
		
		}
		
		// DL Restrictions
		ExtendedIterator<OntClass> it2 = ontClass.listSuperClasses();
		while(it2.hasNext()) {
			OntClass r = it2.next();
			loadDLRestriction(graph, applyToOntClass, r);			
		}
		
		Iterator<Statement> eIt = ontModel.listStatements(null, OWL.equivalentClass, ontClass).andThen(ontModel.listStatements(ontClass, OWL.equivalentClass, (RDFNode) null));
		while(eIt.hasNext()) {
			Statement stmt = eIt.next();
			OntClass r = stmt.getSubject().getURI().compareTo(ontClass.getURI()) == 0 ? stmt.getObject().as(OntClass.class) : stmt.getSubject().as(OntClass.class);
			if(r.isURIResource() && r.getURI().compareTo(applyToOntClass.getURI()) != 0 && !loadDLRestriction(graph, ontClass, r)) loadRestrictions(graph, r, applyToOntClass);	
		}
	}
	
	private boolean isClosure(OntClass c, Property prop) {
		StmtIterator stmtIt = c.listProperties(ontModel.getProperty("http://compflow.pt/flowOntologyExt/closure"));
		while(stmtIt.hasNext()) {
			Statement stmt = stmtIt.next();
			if(prop.getURI().compareTo(stmt.getObject().asLiteral().getString()) == 0) return true;
		}
		return false;
	}
	
	private boolean loadDLRestriction(OntModelGraphWrapper graph, OntClass n, OntClass r) {
		
		if (r.isRestriction()) {
			com.hp.hpl.jena.ontology.Restriction rest = r.asRestriction();
						
			RDFNode rangeNode = rest.getPropertyValue(OWL2.onDataRange);
			if(rangeNode == null)
				rangeNode = rest.getPropertyResourceValue(OWL2.onClass);
			OntProperty propNode = rest.getOnProperty();
				
			int exactCardinality = -1;
			int minCardinality = -1;
			int maxCardinality = -1;
				
			if(rest.isAllValuesFromRestriction()) 
				rangeNode = rest.asAllValuesFromRestriction().getAllValuesFrom();
			
			else if(rest.isSomeValuesFromRestriction())
				rangeNode = rest.asSomeValuesFromRestriction().getSomeValuesFrom();
			
			else if(rest.isCardinalityRestriction())
				exactCardinality = rest.asCardinalityRestriction().getCardinality();
			
			else if(rest.isMaxCardinalityRestriction())
				maxCardinality = rest.asMaxCardinalityRestriction().getMaxCardinality();	
			
			else if(rest.isMinCardinalityRestriction()) 
				minCardinality = rest.asMinCardinalityRestriction().getMinCardinality();
			//OWL2 fix
			else if(rest.hasProperty(OWL2.qualifiedCardinality)) 
				exactCardinality = rest.getPropertyValue(OWL2.qualifiedCardinality).asLiteral().getInt();
						
			else if(rest.hasProperty(OWL2.maxQualifiedCardinality)) 
				maxCardinality = rest.getPropertyValue(OWL2.maxQualifiedCardinality).asLiteral().getInt();
		
			else if(rest.hasProperty(OWL2.minQualifiedCardinality))
				minCardinality = rest.getPropertyValue(OWL2.minQualifiedCardinality).asLiteral().getInt();
			
			else if(rest.isHasValueRestriction())
				rangeNode = rest.asHasValueRestriction().getHasValue();
				
			if(rangeNode != null) {
				if(type == OBJECTPROPERTY_GRAPH && (rangeNode.canAs(OntClass.class) || propNode.canAs(ObjectProperty.class) || (rangeNode.isResource() && rangeNode.asResource().getURI().compareToIgnoreCase(OWL.Thing.getURI()) == 0)))	// detect this from the range (type may be simply property)
					loadEdge(graph, n, rangeNode.asResource(), propNode, false, rest.isHasValueRestriction(), rest.isSomeValuesFromRestriction(), rest.isAllValuesFromRestriction(), exactCardinality, minCardinality, maxCardinality);	
				else if(type == DATATYPEPROPERTY_GRAPH && (propNode.canAs(DatatypeProperty.class) || (rangeNode.isResource() && (rangeNode.asResource().getNameSpace().compareToIgnoreCase(XSD.getURI()) == 0 || rangeNode.asResource().getURI().compareToIgnoreCase(RDFS.Literal.getURI()) == 0))))
					try {
						graph.addRestrictionEdge(n, propNode, rangeNode.isLiteral() ? graph.getModel().createResource(URLEncoder.encode(rangeNode.asLiteral().getString(), "UTF-8")) : rangeNode.asResource(), false, rest.isHasValueRestriction(), rest.isSomeValuesFromRestriction(), rest.isAllValuesFromRestriction(), exactCardinality, minCardinality, maxCardinality, 
								isClosure(n, propNode));
					} catch (UnsupportedEncodingException e) {
						throw new RuntimeException("Unable to create URI for Literal Node in Graph", e);
					}
			}
	
			return true;
		} 
		
		if (r.isIntersectionClass()) {
			IntersectionClass ic = r.asIntersectionClass();
			for (ExtendedIterator<? extends OntClass> it = ic.listOperands(); it.hasNext();) {
				loadDLRestriction(graph, n, it.next());
			}
			return true;
		}
		
		if (r.isUnionClass()) {
			UnionClass uc = r.asUnionClass();
			for (ExtendedIterator<? extends OntClass> it = uc.listOperands(); it.hasNext();) {
				loadDLRestriction(graph, n, it.next());
			}
			return true;
		}
		
		return false;
	}
		
	private void loadEdge(OntModelGraphWrapper graph, OntClass ontClass, Resource range, Property property, boolean isPropertyCentric, boolean isHasValue, boolean isSome, boolean isOnly, int exactCard, int minCard, int maxCard) {
		List<Resource> ranges = new ArrayList<Resource>();
		processRange(range, ranges, new ArrayList<Resource>());
		
		for(Resource r : ranges) {							
			graph.addRestrictionEdge(ontClass, property, r, isPropertyCentric, isHasValue, isSome, isOnly, exactCard, minCard, maxCard, isClosure(ontClass, property));
		}
	}	

	// the analyzed semantics in this method are very important!
	private void processRange(Resource c, List<Resource> list, List<Resource> processedList) {
		if(processedList.contains(c)) return;
		processedList.add(c);
		
		if(c.canAs(OntClass.class)) {
			OntClass cc = c.as(OntClass.class);
			ExtendedIterator<OntClass> equivIt = cc.listEquivalentClasses();
			while (equivIt.hasNext()) {
				OntClass ontClass = equivIt.next();
				processRange(ontClass, list, processedList);
			}
			
			if (c.isURIResource()) {
				list.add(c);
				return;
			}
			
			// TODO add support for intersections in property ranges
			// TODO add support for anonymous classes specified through direct property restrictions (hasPet some (Pet and hasBloodType value cold)) ...
			if (cc.isUnionClass()) {
				UnionClass uc = cc.asUnionClass();
	
				for (ExtendedIterator<? extends OntClass> it = uc.listOperands(); it.hasNext();) {
					OntClass ucm = (OntClass) it.next();
					processRange(ucm, list, processedList);
				}
				
				return;
			}
		} else {
			if (c.isURIResource()) {
				list.add(c);
				return;
			}
		}
	}
		
	public OntModel getOntModel() {
		return this.ontModel;
	}
	
	public OntModelGraphWrapperBuilder setOntModel(String ontologyPath) {
		this.ontModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_DL_MEM);
		ontModel.read(ontologyPath);
		return this;
	}
	
	public OntModelGraphWrapperBuilder setOntModel(OntModel ontModel) {
		this.ontModel = ontModel;
		return this;
	}

	public Onto2FlowModel getTargetModel() {
		return targetModel;
	}

	public void setTargetModel(Onto2FlowModel targetModel) {
		this.targetModel = targetModel;
	}

	public int getType() {
		return type;
	}

	public void setType(int type) {
		this.type = type;
	}

	public String getGraphUri() {
		return graphUri;
	}

	public void setGraphUri(String graphUri) {
		this.graphUri = graphUri;
	}
				
}
